Описание класса

Класс является типом данных, определяемым пользователем. Он должен представлять собой одну логическую сущность, например, являться моделью реального объекта или процесса. Элементами класса являются данные и функции, предназначенные для их обработки.

Описание класса содержит ключевое слово class, за которым следует его имя, а далее в фигурных скобках — тело класса, то есть список его элементов. Кроме того, для класса можно задать его базовые классы (предки) и ряд необязательных атрибутов и спецификаторов, определяющих различные характеристики класса:

[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]
тело_класса

Обязательными являются только ключевое слово class, имя и тело класса. Тело класса — это список описаний его элементов, заключенный в фигурные скобки. Список может быть пустым, если класс не содержит ни одного элемента. Таким образом, простейшее описание класса может выглядеть так:

class Simple {}

Спецификаторы определяют свойства класса, а также доступность класса для других элементов программы. Возможные значения спецификаторов перечислены в таблице 1. Класс можно описывать непосредственно внутри пространства имен или внутри другого класса. В последнем случае класс называется вложенным.


Таблица 1 - Спецификаторы класса

Спецификатор

Описание

1

new

Используется для вложенных классов. Задает новое описание класса взамен унаследованного от предка. Применяется в иерархиях объектов

2

public

Доступ не ограничен

3

protected

Используется для вложенных классов. Доступ только из элементов данного и производных классов

4

internal

Доступ только из данной программы (сборки)

5

protected
internal

Доступ только из данного и производных классов или из данной программы (сборки)

6

private

Используется для вложенных классов. Доступ только из элементов класса, внутри которого описан данный класс

7

abstract

Абстрактный класс. Применяется в иерархиях объектов, рассматривается в главе 8

8

sealed

Бесплодный класс. Применяется в иерархиях объектов, рассматривается в главе 8

9

static

Статический класс. Введен в версию языка 2.0. Рассматривается в разделе "Конструкторы"


Спецификаторы 2–6 называются спецификаторами доступа. Они определяют, откуда можно непосредственно обращаться к данному классу. Спецификаторы доступа могут присутствовать в описании только в вариантах, приведенных в таблице, а также могут комбинироваться с остальными спецификаторами.

Исключая вложенные классы, для остальных классов допускаются два спецификатора: public и internal (по умолчанию).

Класс определяет характеристики и поведение некоторого множества объектов этого класса, называемых экземплярами, или объектами, класса. Объекты создаются явным или неявным образом, то есть либо программистом, либо системой. Программист создает экземпляр класса с помощью операции new, например:

Simple a = new Simple();// создание экземпляра класса Simple
Simple b = new Simple();//создание другого экземпляра класса Simple

Память под объекты выделяется в куче (область памяти под ссылочные значения). Для каждого объекта при его создании в памяти выделяется отдельная область, в которой хранятся его данные. Кроме того, в классе могут присутствовать статические элементы, которые существуют в единственном экземпляре для всех объектов класса. Часто статические данные называют данными класса, а остальные — данными экземпляра.

Функциональные элементы класса всегда хранятся в единственном экземпляре. Для работы с данными класса используются методы класса ( статические методы ), для работы с данными экземпляра — методы экземпляра, или просто методы.

Основные элементы класса - это поля и методы. Кроме того, в классе можно задавать другие элементы такие как: свойства, события, индексаторы, операции, конструкторы, деструкторы, а также типы ( рисунок 7.1).

Рисунок 8.1 - Состав класса


Ниже приведено краткое описание всех элементов класса, изображенных на рисунке:

Константы класса хранят неизменяемые значения, связанные с классом.

Методы реализуют вычисления или другие действия, выполняемые классом или экземпляром.

Свойства определяют характеристики объектов класса в совокупности со способами их задания и получения, то есть методами записи и чтения.

Конструкторы реализуют действия по инициализации экземпляров или класса в целом.

Деструкторы определяют действия, которые необходимо выполнить до того, как объект будет уничтожен.

Индексаторы обеспечивают возможность доступа к элементам класса по их порядковому номеру (индексу).

Операции задают действия с объектами с помощью знаков операций.

События определяют уведомления, которые может генерировать класс.

Типы — это типы данных, внутренние по отношению к классу.

Прежде чем начать изучение элементов класса, необходимо поговорить о присваивании и сравнении объектов.

Механизм выполнения присваивания один и тот же для величин любого типа, как ссылочного, так и значимого, однако результаты различаются. При присваивании значения копируется значение, а при присваивании ссылки — ссылка, поэтому после присваивания одного объекта другому мы получим две ссылки, указывающие на одну и ту же область памяти (рисунок 8.2).

Рисунок 8.2 - Присваивание объектов


Аналогичная ситуация с операцией проверки на равенство. Величины значимого типа равны, если равны их значения. Величины ссылочного типа равны, если они ссылаются на одни и те же данные (на рисунке объекты b и c равны, но a не равно b даже при равенстве их значений или если они обе равны null ).


Данные: поля и константы

Данные, содержащиеся в классе, могут быть переменными или константами и задаются в соответствии с правилами. Переменные, описанные в классе, называются полями класса. При описании элементов класса можно также указывать атрибуты и спецификаторы, задающие различные характеристики элементов. Синтаксис описания элемента данных:

[ атрибуты ] [ спецификаторы ] [ const ] тип имя [ = начальное_значение ]

Возможные спецификаторы полей и констант перечислены в таблице 2. Для констант можно использовать только спецификаторы 1–6.


Таблица 2 - Спецификаторы полей и констант класса

Спецификатор

Описание

1

new

Новое описание поля, скрывающее унаследованный элемент класса

2

public

Доступ к элементу не ограничен

3

protected

Доступ только из данного и производных классов

4

internal

Доступ только из данной сборки

5

protected
internal

Доступ только из данного и производных классов и из данной сборки

6

private

Доступ только из данного класса

7

abstract

Одно поле для всех экземпляров класса

8

sealed

Поле доступно только для чтения

9

static field

Поле может изменяться другим процессом или системой


По умолчанию элементы класса считаются закрытыми ( private ). Для полей класса этот вид доступа является предпочтительным. Все методы класса имеют непосредственный доступ к его закрытым полям.

Внимание

Поля, описанные со спецификатором static, а также константы существуют в единственном экземпляре для всех объектов класса, поэтому к ним обращаются не через имя экземпляра, а через имя класса. Если класс содержит только статические элементы, экземпляр класса создавать не требуется. Именно этим фактом мы пользовались во всех предыдущих листингах.

Обращение к полю класса выполняется с помощью операции доступа (точка). Справа от точки задается имя поля, слева — имя экземпляра для обычных полей или имя класса для статических. В листинге 1 приведен пример простого класса Simple и два способа обращения к его полям.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleClasses1
{
        class Simple
        {
            // Поле данных.
            public int i = 1;
            // Константа.
            public const double d = 2.5;
            // Статическое поле класса.
            public static string s = " Simple ";
            // Закрытое поле данных.
            double z = 3;
        }
    class Program
    {

        static void Main(string[] args)
        {
            // Создание экземпляра класса Simple.
            Simple x = new Simple();
            // x.i - обращение к полю класса.
            Console.WriteLine(x.i);
            // Simple.d - обращение к константе.
            Console.WriteLine(Simple.d);
            // Обращение к статическому полю.
            Console.WriteLine(Simple.s); 
        }
    }
}

Листинг 1. Класс Simple, содержащий поля и константу


Поля со спецификатором readonly предназначены только для чтения. Установить значение такого поля можно либо при его описании, либо в конструкторе (конструкторы рассматриваются далее).


Методы

Методы определяют поведение класса. Метод — это функциональный элемент класса. Он реализует вычисления или другие действия, выполняемые классом или экземпляром.

Метод представляет собой законченный фрагмент кода, к которому можно обратиться по имени. Он описывается один раз, а вызываться может столько раз, сколько необходимо. Один и тот же метод может обрабатывать различные данные, переданные ему в качестве аргументов.

Синтаксис метода:

[ атрибуты ] [ спецификаторы ] тип имя_метода ( [ параметры ] )
тело_метода

Первая часть описания представляет собой заголовок метода или сигнатуру. Тело метода задаёт действия, выполняемые методом. Оно чаще всего представляет собой блок.

При описании методов можно использовать спецификаторы 1–7 из Таблицы 2, имеющие тот же смысл, что и для полей, а также спецификаторы virtual, sealed, override, abstract и extern, которые будут рассмотрены по мере необходимости. Методы со спецификатором доступа public составляют интерфейс класса — то, с чем работает пользователь.

Пример простейшего метода:


// Метод для получения значения z.
            public double Get_z() 
            {
                return z;
            }            


Тип определяет, значение какого типа вычисляется с помощью метода. Часто употребляется термин "метод возвращает значение". Если метод не возвращает никакого значения, в его заголовке задается тип void, а оператор return отсутствует.

С помощью параметров метод обменивается данными с вызывающим его методом. Параметр представляет собой локальную переменную, которая при вызове метода принимает значение соответствующего фактического параметра (параметра переданного при вызове). Область действия параметра — весь метод.

Например, чтобы выделить целую часть вещественной величины a, мы передаем ее в качестве аргумента в метод Truncate класса Math, а чтобы вывести значение переменной r на экран, мы передаем ее в метод WriteLine класса Console:


            double a = 2.56;
            double r = Math.Truncate(a);
            Console.WriteLine(r);

При этом метод Truncate возвращает в точку своего вызова вещественное значение - целую часть a, которое присваивается переменной r, а метод WriteLine ничего не возвращает. Иллюстрация вызова метода приведена на рисунке 8.3.

Рисунок 8.3 - Вызов метода


Внимание

Метод, не возвращающий значение, вызывается отдельным оператором, а метод, возвращающий значение, — в составе выражения в правой части оператора присваивания.

Параметры, описываемые в заголовке метода, определяют множество значений аргументов, которые можно передавать в метод. Список аргументов при вызове как бы накладывается на список параметров, поэтому они должны попарно соответствовать друг другу. Для каждого параметра должны задаваться его тип и имя. Например, заголовок метода Sin выглядит следующим образом:

public static double Sin( double a );

Имя метода вместе с количеством, типами и спецификаторами его параметров представляет собой сигнатуру метода. В классе не должно быть методов с одинаковыми сигнатурами.

В листинге 2 в класс Simple добавлены методы установки и получения значения поля z.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleClasses3
{
        class Simple
        {
            // Поле данных.
            public int i = 1;
            // Константа.
            public const double d = 2.5;
            // Статическое поле класса.
            public static string s = " Simple ";
            // Закрытое поле данных.
            double z = 3;
            // Метод для получения значения z.
            public double Get_z()
            {
                return z;
            }
            // Метод установки поля z.
            public void Set_z(double z_)
            {
                z = z_;
            }
        }
    class Program
    {

        static void Main(string[] args)
        {
            // Создание экземпляра класса Simple.
            Simple x = new Simple();
            // Вызов метода установки поля z объекта x значением 10.
            x.Set_z(10);
            // Вызов метода получения поля z объекта x.
            Console.WriteLine(x.Get_z());
        }
    }
}

Листинг 2. Простейшие методы


Параметры методов

При вызове метода выполняются следующие действия:

1. Вычисляются выражения, стоящие на месте аргументов.

2. Выделяется память под параметры метода в соответствии с их типом.

3. Каждому из параметров сопоставляется соответствующий аргумент (аргументы как бы накладываются на параметры и замещают их).

4. Выполняется тело метода.

5. Если метод возвращает значение, оно передается в точку вызова; если метод имеет тип void, управление передается на оператор, следующий после вызова.

При этом проверяется соответствие типов аргументов и параметров и при необходимости выполняется их преобразование. При несоответствии типов выдается диагностическое сообщение. Листинг 3 иллюстрирует этот процесс.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleClasses4
{
    class Example
    {   
        // Метод выбора минимального значения.
        public static int Min(int a, int b)  
        {
            if ( a < b ) return a;
            else         return b;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            int a = 2, b = 4;
            // Вызов метода Min.
            int r = Example.Min( a, b );
            // Результат: 2.     
            Console.WriteLine( r );              

            short c = 3, d = 4;
            // вызов метода Min.
            int w = Example.Min(c, d);
            // Результат: 3.   
            Console.WriteLine( w );              

            // вызов метода Min.
            int v = Example.Min(a + c, d / 2 * b);   
            // Результат: 5.   
            Console.WriteLine( v );
        }
    }
}

Листинг 3. Передача параметров методу.


Внимание

Главное требование при передаче параметров состоит в том, что аргументы при вызове метода должны записываться в том же порядке, что и в заголовке метода, и должно существовать неявное преобразование типа каждого аргумента к типу соответствующего параметра. Количество аргументов должно соответствовать количеству параметров. Иллюстрация приведена на рисунке 8.4.

Рисунок 8.4 - Соответствие параметров при вызове метода


Существуют два способа передачи параметров: по значению и по ссылке.

При передаче по значению метод получает копии значений аргументов, и операторы метода работают с этими копиями. Доступа к исходным значениям аргументов у метода нет, а, следовательно, нет и возможности их изменить.

При передаче по ссылке ( по адресу ) метод получает копии адресов аргументов, он осуществляет доступ к ячейкам памяти по этим адресам и может изменять исходные значения аргументов, модифицируя параметры.

В C# для обмена данными между вызывающей и вызываемой функциями предусмотрено четыре типа параметров:

• параметры-значения;

• параметры-ссылки — описываются с помощью ключевого слова ref ;

• выходные параметры — описываются с помощью ключевого слова out ;

• параметры-массивы — описываются с помощью ключевого слова params.

Ключевое слово предшествует описанию типа параметра. Если оно опущено, параметр считается параметром-значением. Параметр-массив может быть только один и должен располагаться последним в списке, например:

public int Calculate( int a, ref int b, out int c, params int[] d ) …

Параметры-значения

Параметр-значение описывается в заголовке метода следующим образом:

тип имя

Пример заголовка метода, имеющего один параметр-значение целого типа:

void P( int x )

Имя параметра может быть произвольным. Параметр х представляет собой локальную переменную, которая получает свое значение из вызывающей функции при вызове метода. В метод передается копия значения аргумента.

Механизм передачи следующий: из ячейки памяти, в которой хранится переменная, передаваемая в метод, берется ее значение и копируется в специальную область памяти — область параметров. Метод работает с этой копией. По завершении работы метода область параметров освобождается. Этот способ годится только для передачи в метод исходных данных.

При вызове метода на месте параметра, передаваемого по значению, может находиться выражение, для типа которого существует неявное преобразование типа выражения к типу параметра.

Например, пусть в вызывающей функции описаны переменные и им до вызова метода присвоены значения:

int x = 1;
sbyte c = 1;
ushort y = 1;

Тогда следующие вызовы метода Р, заголовок которого был описан ранее, будут синтаксически правильными:

P( x ); P( c ); P( y ); P( 200 ); P( x / 4 + 1 );

Параметры-ссылки

Признаком параметра-ссылки является ключевое слово ref перед описанием параметра:

ref тип имя

Пример заголовка метода, имеющего один параметр-ссылку целого типа:

void P( ref int x )

При вызове метода в область параметров копируется адрес аргумента, и метод через него имеет доступ к ячейке, в которой хранится аргумент. Метод работает непосредственно с переменной из вызывающей функции и, следовательно, может ее изменить, поэтому если в методе требуется изменить значения параметров, они должны передаваться только по ссылке.

Внимание

При вызове метода на месте параметра-ссылки может находиться только ссылка на инициализированную переменную точно того же типа. Перед именем параметра указывается ключевое слово ref.

Проиллюстрируем передачу параметров-значений и параметров-ссылок на примере (листинг 4).


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleMethodParams
{
    class Program
    {
        static void P(int a, ref int b)
        {
            a = 11; b = 22;
            Console.WriteLine("Внутри метода \ta = {0} \tb = {1}", a, b);
        }
        static void Main(string[] args)
        {
            int a = 2, b = 4;
            Console.WriteLine("До вызова \tа = {0} \tb = {1}", a, b);
            P(a, ref b);
            Console.WriteLine("После вызова \tа = {0} \tb = {1}", a, b);
        }
    }
}

Листинг 4. Параметры-значения и параметры-ссылки


Результаты работы этой программы (рисунок 8.5):

Рисунок 8.5 - Результаты работы программы


Несколько иная картина получится, если передавать в метод не величины значимых типов, а экземпляры классов, то есть величины ссылочных типов. Для простоты можно считать, что объекты всегда передаются по ссылке.

Выходные параметры

Довольно часто возникает необходимость в методах, которые формируют несколько величин. В этом случае становится неудобным ограничение параметров-ссылок: необходимость присваивания значения аргументу до вызова метода. Это ограничение снимает спецификаторout. Параметру, имеющему этот спецификатор, должно быть обязательно присвоено значение внутри метода.

Изменим описание второго параметра в листинге 4 так, чтобы он стал выходным (листинг 5).


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleOutParam
{
    class Program
    {
        static void P(int a, out int b)
        {
            a = 11; b = 22;
            Console.WriteLine("Внутри метода \ta = {0} \tb = {1}", a, b);
        }
        static void Main(string[] args)
        {
            int a = 2, b;
            P(a, out b);
            Console.WriteLine("После вызова \tа = {0} \tb = {1}", a, b);
        }
    }
}

Листинг 5. Выходные параметры


При вызове метода перед соответствующим параметром тоже указывается ключевое слово out (рисунок 8.6).

Рисунок 8.6 – Работа программы (листинг 5)


Ключевое слово this

Каждый объект содержит свой набор полей класса. Методы находятся в памяти в единственном экземпляре и используются всеми объектами совместно, поэтому необходимо обеспечить работу методов с полями именно того объекта, для которого они были вызваны. Для этого в любой нестатический метод автоматически передается скрытый параметр this, в котором хранится ссылка на вызвавший функцию объект. Этот процесс иллюстрирует рисунок 8.7.

Рисунок 8.7 - Передача методу скрытого параметра this


В явном виде параметр this применяется для того, чтобы возвратить из метода ссылку на вызвавший объект, а также для идентификации поля в случае, если его имя совпадает с именем параметра метода, например:


    class Simple
    {
        double y;
        // Метод возвращает ссылку на экземпляр.
        public Simple T()         
        {
            return this;
        }
        public void Sety(double y)
        {
            // Полю y присваивается значение параметра y.
            this.y = y;         
        }
    }



Конструкторы

Конструктор предназначен для инициализации объекта. Он вызывается автоматически при создании объекта класса с помощью операции new. Имя конструктора совпадает с именем класса. Ниже перечислены свойства конструкторов

• Конструктор не возвращает значение, даже типа void.

• Класс может иметь несколько конструкторов с разными параметрами для разных видов инициализации.

• Если программист не указал ни одного конструктора или какие-то поля не были инициализированы, полям значимых типов присваивается нуль, полям ссылочных типов — значение null.

• Конструктор, вызываемый без параметров, называется конструктором по умолчанию.

До сих пор мы задавали начальные значения полей класса при описании класса. Это удобно в том случае, когда для всех объектов класса начальные значения некоторого поля одинаковы. Если же при создании объектов требуется присваивать полю разные значения, это следует делать в конструкторе. В листинге 6 в класс Simple добавлен конструктор, а поля сделаны закрытыми.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleConstructor
{
    class Simple
    {
        // Конструктор с параметрами.
        public Simple ( int a, double z )          
        {
            this.a = a;
            this.z = z;
        }

        // Метод получения поля y
        public double Get_z()                    
        {
            return z; 
        }

        int a;   
        double z;
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // Вызов конструктора.
            Simple a = new Simple ( 10, 2.2 );
            // Результат: 2,2.
            Console.WriteLine( a.Get_z() );
            // Вызов конструктора.
            Simple b = new Simple ( 1, 6.54 );
            // Результат: 6,54.
            Console.WriteLine( b.Get_z() );       
        }
    }
}

Листинг 6. Класс с конструктором


Часто бывает удобно задать в классе несколько конструкторов, чтобы обеспечить возможность инициализации объектов разными способами. Все конструкторы должны иметь разные сигнатуры.

Если один из конструкторов выполняет какие-либо действия, а другой должен делать то же самое плюс еще что-нибудь, удобно вызвать первый конструктор из второго. Для этого используется уже известное вам ключевое слово this в другом контексте, например:


    class Simple
    {
        
        // Конструктор 1.
        public Simple ( int a )                         
        {
            this.a = a;
        }
        
        // Вызов конструктора 1.
        public Simple ( int a, double y ) : this( a )   
        {
            this.y = y;
        }
        ...
    }

Конструкция, находящаяся после двоеточия, называется инициализатором.

Как вы помните, все классы в C# имеют общего предка — класс object. Конструктор любого класса, если не указан инициализатор, автоматически вызывает конструктор своего предка.

До сих пор речь шла об "обычных" конструкторах, или конструкторах экземпляра. Существует второй тип конструкторов — статические конструкторы, или конструкторы класса. Конструктор экземпляра инициализирует данные экземпляра, конструктор класса — данные класса.

Статический конструктор не имеет параметров, его нельзя вызвать явным образом. Система сама определяет момент, в который требуется его выполнить.

Некоторые классы содержат только статические данные и, следовательно, создавать экземпляры таких объектов не имеет смысла. В версию 2.0 введена возможность описывать статический класс, то есть класс с модификатором static. Экземпляры такого класса создавать запрещено, и кроме того, от него запрещено наследовать. Все элементы такого класса должны явным образом объявляться с модификатором static (константы и вложенные типы классифицируются как статические элементы автоматически). В листинге 7 приведен пример статического класса.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleStatClass
{
    static class Simple
    {
        static int a = 10;
        static double b = 2.5;
        const double pi = 3.1415;

        public static void Print ()
        {
            Console.WriteLine("a = " + a );
            Console.WriteLine("b = " + b);
            Console.WriteLine("pi = " + pi);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Simple.Print();
        }
    }
}

Листинг 7. Статический класс (начиная с версии 2.0)

В качестве "сквозного" примера, на котором будет демонстрироваться работа с различными элементами класса, создадим класс, моделирующий простую дробь. Для этого требуется задать значения числителя и знаменателя и операцию сложения.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleFrac1
{
    class TFrac
    {
        int num;
        int denom;
        public override string ToString()
        {
            string s = string.Format("{0}/{1}", num, denom);
            return s;
        }
        public TFrac(int n, int d = 1)
        {
            this.num = n;
            this.denom = d;
            Reduce();
        }
        public TFrac(string s = "0/1")
        {

            int n = s.IndexOf('/');
            if (n == -1)
            {
                this.num = int.Parse(s);
                this.denom = 1;
            }
            else
            {
                this.num = int.Parse(s.Substring(0, n));
                this.denom = int.Parse(s.Substring(n + 1));
            }
            Reduce();
        }
        public TFrac Add(TFrac b)
        {
            int n = this.num * b.denom + b.num * this.denom;
            int d = this.denom * b.denom;
            return new TFrac(n, d);
        }
        static int nod(int a, int b)
        {
            int t;
            a = Math.Abs(a);
            b = Math.Abs(b);
            if (a > b) { t = a; a = b; b = t; }
            while (a != 0 & a != b)
            {
                if (a <= b) { b = b % a; }
                else { t = a; a = b; b = t; }
            }
            return b;
        }
        void Reduce()
        {
            int t = nod(this.num, this.denom);
            this.num /= t;
            this.denom /= t;
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            TFrac a = new TFrac(1, 5);
            Console.WriteLine(a.ToString());
            TFrac b = new TFrac("3/5");
            Console.WriteLine(b.ToString());
            TFrac c = a.Add(b);
            Console.WriteLine(c.ToString());
        }
    }
}

Листинг 8. Класс TFrac


Результат работы программы (рисунок 8.8):

Рисунок 8.8 - Результат работы программы (листинг 8)


Свойства

Свойства служат для организации доступа к полям класса. Как правило, свойство связано с закрытым полем класса и определяет методы его получения и установк. Синтаксис свойства:

[ атрибуты ] [ спецификаторы ] тип имя_свойства
{
[ get код_доступа ]
[ set код_доступа ]
}

Значения спецификаторов для свойств и методов аналогичны. Чаще всего свойства объявляются со спецификатором public. Код доступа представляет собой блоки операторов, которые выполняются при получении ( get ) или установке ( set ) свойства. Может отсутствовать либо часть get, либо set, но не обе одновременно.

Если отсутствует часть set, свойство доступно только для чтения (read-only), если отсутствует часть get, свойство доступно только для записи (write-only). В версии C# 2.0 введена возможность задавать разный уровень доступа для частей get и set.

Пример описания свойств:


public class Button: Control  
{
// Закрытое поле, с которым связано свойство 
    private string caption;     
// свойство
    public string Caption 
    {     
       // способ получения свойства
       get {                    
          return caption;
       }
       // способ установки свойства
       set {                      
          if (caption != value) {
             caption = value;
          }
       }
    }
    ...
}

Метод записи обычно содержит действия по проверке допустимости устанавливаемого значения, метод чтения может содержать, например, поддержку счетчика обращений к полю.

В программе свойство выглядит как поле класса, например:

Button ok = new Button();
// вызывается метод установки свойства
ok.Caption = "OK";
// вызывается метод получения свойства
string s = ok.Caption;

При обращении к свойству автоматически вызываются указанные в нем методы чтения и установки.

Синтаксически чтение и запись свойства выглядят почти как методы. Метод get должен содержать оператор return. В методе set используется параметр со стандартным именем value, который содержит устанавливаемое значение.

Добавим в класс TFrac, описанный в листинге 8, свойства, позволяющие работать с закрытыми полями этого класса. Код класса несколько разрастется, зато упростится его использование.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleFrac1
{
    class TFrac
    {
        int num;
        int denom;
        // Свойство для работы с полем num.
        public int Num
        {
            get { return num; }
            set { if (num != value) num = value; }
        }
        // Свойство для работы с полем denom.
        public int Denom
        {
            get { return denom; }
            set { if (denom != value) denom = value; }
        }
        // Свойство для работы с полями num, denom.
        public string Frac
        {
            get    
            {
                string s = string.Format("{0}/{1}", num, denom);
                return s;
            }
        }
        public TFrac(int n, int d = 1)
        {
            this.num = n;
            this.denom = d;
            Reduce();
        }
        public TFrac(string s = "0/1")
        {

            int n = s.IndexOf('/');
            if (n == -1)
            {
                this.num = int.Parse(s);
                this.denom = 1;
            }
            else
            {
                this.num = int.Parse(s.Substring(0, n));
                this.denom = int.Parse(s.Substring(n + 1));
            }
            Reduce();
        }
        public TFrac Add(TFrac b)
        {
            int n = this.num * b.denom + b.num * this.denom;
            int d = this.denom * b.denom;
            return new TFrac(n, d);
        }
        static int nod(int a, int b)
        {
            int t;
            a = Math.Abs(a);
            b = Math.Abs(b);
            if (a > b) { t = a; a = b; b = t; }
            while (a != 0 & a != b)
            {
                if (a <= b) { b = b % a; }
                else { t = a; a = b; b = t; }
            }
            return b;
        }
        void Reduce()
        {
            int t = nod(this.num, this.denom);
            this.num /= t;
            this.denom /= t;
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            TFrac a = new TFrac(1, 5);
            // Используем свойство.
            Console.WriteLine(a.Frac);
            // Используем свойство.
            a.Num = 3;
            // Используем свойство.
            a.Denom = 7;
            // Используем свойство.
            Console.WriteLine(a.Frac);
        }
    }
}

Листинг 9. Класс TFrac со свойствами


Результат работы программы (рисунок 8.9):

Рисунок 8.9 - Результат работы программы (листинг 9)


Контрольные вопросы

1. Перечислите и опишите элементы класса в C#.

2. Опишите способы передачи параметров в методы.

3. Запишите алгоритм вычисления чисел Фибоначчи с помощью рекурсии.

4. Для чего в классе может потребоваться несколько конструкторов?

5. Как можно вызвать один конструктор из другого? Зачем это нужно?

6. Что такое this? Что в нем хранится, как он используется?

7. Что такое деструктор? Гарантирует ли среда его выполнение?

8. Какие действия обычно выполняются в части set свойства?

9. Может ли свойство класса быть не связанным с его полями?

10. Можно ли описать разные спецификаторы доступа к частям get и set свойства?